<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>虚拟列表-固定高度</title>
    <!-- https://blog.csdn.net/qiwoo_weekly/article/details/134301493 -->
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }

      .operation {
        position: fixed;
        top: 0;
        left: 0;
        width: 220px;
        padding: 12px;
        box-sizing: border-box;
        background: #60606090;
        border-top-right-radius: 12px;
        border-bottom-right-radius: 12px;
      }

      body {
        display: flex;
        height: 100vh;
      }

      .container {
        margin: auto;
        width: 300px;
        height: 500px;
        border: 1px solid black;
        overflow: auto;
      }

      .container .item {
        height: 100px;
        background-color: #888;
        color: white;
        font-size: 2em;
        border: 1px solid red;
      }
    </style>
  </head>
  <body>
    <div class="operation">
      <p><button id="add">Add</button></p>

      <p><button id="add100">Add 100 items</button></p>
    </div>
    <div class="container">
      <div class="wrapper"></div>
    </div>
    <script>
      const container = document.querySelector(".container");
      const wrapper = document.querySelector(".wrapper");
      const containerHeight = 500;
      const itemHeight = 100;
      // 表示可视区域最大能容纳的个数
      const containerViewCount = Math.ceil(containerHeight / itemHeight);
      let wrapperElements = [];
      const createItem = () => {
        const e = document.createElement("div");
        e.classList.add("item");
        e.textContent = `${wrapperElements.length}`;
        return e;
      };
      document.querySelector("#add").addEventListener("click", () => {
        const e = createItem();
        container.appendChild(e);
      });

      document.querySelector("#add100").addEventListener("click", () => {
        wrapperElements.push(...new Array(100).fill(0).map(createItem));
        wrapper.style.height = `${wrapperElements.length * itemHeight}px`;
      });

      container.addEventListener("scroll", (e) => {
        const scrollTop = e.target.scrollTop;
        let startIndex = Math.floor(scrollTop / itemHeight) - 2;
        if (startIndex < 0) {
          startIndex = 0;
        }
        let endIndex = startIndex + containerViewCount + 2;
        if (endIndex > wrapperElements.length - 1) {
          endIndex = wrapperElements.length - 1;
        }

        // 保证上缓冲区多余移除元素使高度稳定
        if (startIndex > 0) {
          const remove = wrapperElements.slice(0, startIndex);
          const paddingTop = remove.length * itemHeight;
          container.style.paddingTop = paddingTop;
          remove.forEach((e) => wrapper.removeChild(e));
          let i = startIndex;
          while (i) {
            wrapperElements.splice(--i, 1);
          }
        }

        wrapperElements
          .slice(startIndex, endIndex + 1)
          .forEach((e) => wrapper.appendChild(e));
      });
    </script>
  </body>
</html>
